package com.roncoo.education.system.service.admin.biz;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Service;

import com.roncoo.education.common.core.base.Result;
import com.roncoo.education.common.core.tools.BeanUtil;
import com.roncoo.education.common.upload.Upload;
import com.roncoo.education.system.dao.MinioUploadingDao;
import com.roncoo.education.system.dao.SysConfigDao;
import com.roncoo.education.system.dao.impl.mapper.entity.MinioUploading;
import com.roncoo.education.system.service.admin.req.FileDto;
import com.roncoo.education.system.service.admin.resp.AdminMinioUploadingFileResp;
import com.roncoo.education.system.service.admin.resp.AdminMinioUploadingResp;

import io.minio.BucketExistsArgs;
import io.minio.ComposeObjectArgs;
import io.minio.ComposeSource;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.ListObjectsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.RemoveObjectArgs;
import io.minio.http.Method;
import io.minio.messages.Item;
import lombok.Getter;
import lombok.SneakyThrows;

/**
 * @author chenlin
 * @since 2021.05.06  17:54
 */
@Service
public class MinioBiz {
	@Autowired
	private MinioUploadingDao uploadingDao;
    @Autowired
    private AdminUploadBiz biz;
    @Autowired
    private SysConfigDao sysConfigDao;
    
    private Upload getMinioUpload() {
    	return biz.getUploadForSysConfig();
    }
    
    private MinioClient getMinioClient() {
    	Upload upload = getMinioUpload();
    	MinioClient minioClient = MinioClient.builder().endpoint(upload.getMinioDomain()).credentials(upload.getMinioAccessKey(), upload.getMinioSecretKey()).build();
    	return minioClient;
    }
    
    public Result<AdminMinioUploadingFileResp> chunkUpload(FileDto dto) {
        // 根据文件MD5作为主键查询,查询文件是否上传过
    	MinioUploading fileEntity = uploadingDao.getById(dto.getFileMd5());
        if (fileEntity != null) {
            // 文件已上传完成，实现秒传
            if (fileEntity.getFileState().equals(FileStateEnum.UPLOAD_SUCCESS.getCode())) {
            	return Result.success(BeanUtil.copyProperties(new AdminMinioUploadingFileResp()
                        .setUploadCode(fileEntity.getFileState())
                        .setFileUrl(fileEntity.getFileUrl()), AdminMinioUploadingFileResp.class));
            }

            // 文件断点续传
            if (fileEntity.getFileState().equals(FileStateEnum.CHUNK_UPLOAD.getCode())) {
                return Result.success(BeanUtil.copyProperties(this.breakpointResume(dto), AdminMinioUploadingFileResp.class));
            }
        }
        AdminMinioUploadingFileResp uploading = this.createUpload(dto);
        MinioUploading minioUploading = new MinioUploading();
        minioUploading.setFileName(dto.getFileName());
        minioUploading.setId(dto.getFileMd5());
        minioUploading.setFileState(FileStateEnum.CHUNK_UPLOAD.getCode());
        uploadingDao.save(minioUploading);
        return Result.success(BeanUtil.copyProperties(uploading, AdminMinioUploadingFileResp.class));
    }
    
    public Result<AdminMinioUploadingFileResp> composeFile(FileDto dto) {
    	Upload upload = getMinioUpload();
        // 分片文件
        List<String> chunks = listObjectNames(upload.getMinioChunkBucket(), dto.getFileMd5());

        if (!composeObject(upload.getMinioChunkBucket(), upload.getMinioBucket(), chunks, dto.getFileName())) {
        	uploadingDao.deleteById(dto.getFileMd5());
            return Result.error("文件合并失败，重新上传");
        }
        String split = "/";
        if (StringUtils.isNotBlank(upload.getMinioDomain()) && upload.getMinioDomain().endsWith("/")) {
        	split = StringUtils.EMPTY;
        }
        String fileUrl = upload.getMinioDomain() + split + upload.getMinioBucket() + "/" + dto.getFileName();
        // 更新数据库文件信息
        MinioUploading minioUploading = new MinioUploading();
        minioUploading.setId(dto.getFileMd5());
        minioUploading.setFileState(FileStateEnum.UPLOAD_SUCCESS.getCode());
        minioUploading.setFileName(dto.getFileName());
        minioUploading.setFileUrl(upload.getMinioDomain() + split + upload.getMinioBucket() + "/" + dto.getFileName());
        uploadingDao.updateById(minioUploading);

        return Result.success(BeanUtil.copyProperties(new AdminMinioUploadingFileResp()
    			.setFileUrl(fileUrl)
    			.setUploadCode(FileStateEnum.UPLOAD_SUCCESS.getCode()), AdminMinioUploadingFileResp.class));
    }
    
    /**
     * 断点续传
     *
     * @param dto
     * @return
     */
    private AdminMinioUploadingFileResp breakpointResume(FileDto dto) {
    	Upload upload = getMinioUpload();
        // 已上传成功的分片
        Map<Integer, String> okChunkMap = mapChunkObjectNames(upload.getMinioAccessKey(), dto.getFileMd5());
        // 没有分片记录创建分片上传
        if (okChunkMap == null) {
            return this.createUpload(dto);
        }
        List<AdminMinioUploadingResp> chunkUploadUrls = new ArrayList<>();
        for (int i = 1; i <= dto.getChunkCount(); i++) {
            // 判断当前分片是否上传完成
            if (!okChunkMap.containsKey(i)) {
                chunkUploadUrls.add(new AdminMinioUploadingResp()
                        .setPartNumber(i)
                        .setUploadUrl(createUploadChunkUrl(upload.getMinioChunkBucket(), dto.getFileMd5(), i)));
            }
        }
        // 分片全部上传完成
        if (chunkUploadUrls.size() == 0) {
            return new AdminMinioUploadingFileResp().setUploadCode(FileStateEnum.CHUNK_UPLOAD_SUCCESS.getCode());
        }

        return new AdminMinioUploadingFileResp()
                .setUploadCode(FileStateEnum.CHUNK_UPLOAD.getCode())
                .setChunkUploadUrls(chunkUploadUrls);
    }
    
    
    /**
     * 新建分片上传
     *
     * @param dto
     * @return
     */
    private AdminMinioUploadingFileResp createUpload(FileDto dto) {
    	Upload upload = getMinioUpload();
    	
        String contentType = MediaTypeFactory.getMediaType(dto.getFileName()).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
    	
        List<AdminMinioUploadingResp> chunkUploadUrls = new ArrayList<>();
        for (int i = 1; i <= dto.getChunkCount(); i++) {
            chunkUploadUrls.add(new AdminMinioUploadingResp()
                    .setPartNumber(i)
                    .setContentType(contentType)
                    .setUploadUrl(createUploadChunkUrl(upload.getMinioChunkBucket(), dto.getFileMd5(), i)));
        }
        return new AdminMinioUploadingFileResp()
                .setUploadCode(FileStateEnum.CHUNK_UPLOAD.getCode())
                .setChunkUploadUrls(chunkUploadUrls);
    }

    /**
     * 判断存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return
     */
    @SneakyThrows
    public boolean isBucketExist(String bucketName) {
        return getMinioClient().bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 存储桶名称
     */
    @SneakyThrows
    public void createBucket(String bucketName) {
    	getMinioClient().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 获取访问对象的外链地址
     *
     * @param bucketName 存储桶名称
     * @param objectName 对象名称
     * @param expiry     过期时间(天) 最大为7天 超过7天则默认最大值
     * @return viewUrl
     */
    @SneakyThrows
    public String getObjectUrl(String bucketName, String objectName, Integer expiry) {
        expiry = expiry * 24 * 60 * 60;
        return getMinioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .bucket(bucketName)
                .object(objectName)
                .expiry(expiry).build());
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @param expiry     过期时间(天) 最大为7天 超过7天则默认最大值
     * @return uploadUrl
     */
    @SneakyThrows
    public String createUploadUrl(String bucketName, String objectName, Integer expiry) {
        expiry = expiry * 24 * 60 * 60;
        return getMinioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                .method(Method.PUT)
                .bucket(bucketName)
                .object(objectName)
                .expiry(expiry).build());
    }

    /**
     * 批量创建分片上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param chunkCount 分片数量
     * @return uploadChunkUrls
     */
    public List<String> createUploadChunkUrlList(String bucketName, String objectMD5, Integer chunkCount) {
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/";
        if (null == chunkCount || 0 == chunkCount) {
            return null;
        }
        List<String> urlList = new ArrayList<>(chunkCount);
        for (int i = 1; i <= chunkCount; i++) {
            String objectName = objectMD5 + i + ".chunk";
            urlList.add(createUploadUrl(bucketName, objectName, 7));
        }
        return urlList;
    }

    /**
     * 创建指定序号的分片文件上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param partNumber 分片序号
     * @return uploadChunkUrl
     */
    public String createUploadChunkUrl(String bucketName, String objectMD5, Integer partNumber) {
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/" + partNumber + ".chunk";
        return createUploadUrl(bucketName, objectMD5, 7);
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀
     * @return objectNames
     */
    @SneakyThrows
    public List<String> listObjectNames(String bucketName, String prefix) {
        Iterable<io.minio.Result<Item>> chunks = getMinioClient().listObjects(ListObjectsArgs.builder()
                .bucket(bucketName)
                .prefix(prefix + "/")
                .build());
        List<String> chunkPaths = new ArrayList<>();
        for (io.minio.Result<Item> item : chunks) {
            chunkPaths.add(item.get().objectName());
        }
        chunkPaths.sort((o1, o2) -> {
            if (o1.length() == o2.length()) {
                return o1.compareTo(o2);
            }
            if (o1.length() > o2.length()) {
                return 1;
            }
            return -1;
        });
        return chunkPaths;
    }

    /**
     * 获取分片文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param objectMd5  对象Md5
     * @return objectChunkNames
     */
    public List<String> listChunkObjectNames(String bucketName, String objectMd5) {
        if (null == objectMd5) {
            return null;
        }
        return listObjectNames(bucketName, objectMd5);
    }

    /**
     * 获取分片名称地址HashMap key=分片序号 value=分片文件地址
     *
     * @param bucketName 存储桶名称
     * @param objectMd5  对象Md5
     * @return objectChunkNameMap
     */
    public Map<Integer, String> mapChunkObjectNames(String bucketName, String objectMd5) {
        if (null == objectMd5) {
            return null;
        }
        List<String> chunkPaths = listObjectNames(bucketName, objectMd5);
        if (null == chunkPaths || chunkPaths.size() == 0) {
            return null;
        }
        Map<Integer, String> chunkMap = new HashMap<>(chunkPaths.size());
        for (String chunkName : chunkPaths) {
            Integer partNumber = Integer.parseInt(chunkName.substring(chunkName.indexOf("/") + 1, chunkName.lastIndexOf(".")));
            chunkMap.put(partNumber, chunkName);
        }
        return chunkMap;
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param chunkBucKetName   分片文件所在存储桶名称
     * @param composeBucketName 合并后的对象文件存储的存储桶名称
     * @param chunkNames        分片文件名称集合
     * @param objectName        合并后的对象文件名称
     * @return true/false
     */
    @SneakyThrows
    public boolean composeObject(String chunkBucKetName, String composeBucketName, List<String> chunkNames, String objectName) {
        List<ComposeSource> sourceObjectList = new ArrayList<>(chunkNames.size());
        for (String chunk : chunkNames) {
            sourceObjectList.add(ComposeSource.builder()
                    .bucket(chunkBucKetName)
                    .object(chunk)
                    .build());
        }
        String contentType = MediaTypeFactory.getMediaType(objectName).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", contentType);
        getMinioClient().composeObject(ComposeObjectArgs.builder()
                .bucket(composeBucketName)
                .object(objectName)
                .headers(headers)
                .sources(sourceObjectList)
                .build());
        // 合并文件后删除分块
        for (String chunk :chunkNames) {
        	getMinioClient().removeObject(RemoveObjectArgs.builder()
                    .bucket(chunkBucKetName)
                    .object(chunk)
                    .build());
        }
        return true;
    }
}

/**
 * @author Chen Lin
 * @date 2022.03.23  10:38 AM
 * @description 文件状态枚举类
 */
@Getter
enum FileStateEnum {
    UPLOAD_SUCCESS(1,"上传完成"),
    CHUNK_UPLOAD(2,"分片上传中"),
    CHUNK_UPLOAD_SUCCESS(3,"分片上传完成")
    ;
    private final Integer code;
    private final String msg;

    FileStateEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
